/**********************************************************************

  Audacity: A Digital Audio Editor

  ExportPCM.cpp

  Dominic Mazzoni

**********************************************************************/

#include <wx/defs.h>

#include <wx/app.h>
#include <wx/dynlib.h>
#include <wx/filename.h>

#include <sndfile.h>

#include "Dither.h"
#include "libraries/lib-file-formats/FileFormats.h"
#include "Mix.h"
#include "Prefs.h"
#include "libraries/lib-tags/Tags.h"
#include "Track.h"
#include "wxFileNameWrapper.h"

#include "libraries/lib-import-export/Export.h"
#include "libraries/lib-import-export/ExportOptionsEditor.h"

#include "libraries/lib-import-export/ExportPluginHelpers.h"
#include "libraries/lib-import-export/ExportPluginRegistry.h"

#include "ExportPCM.h"

#ifdef USE_LIBID3TAG
   #include <id3tag.h>
// DM: the following functions were supposed to have been
// included in id3tag.h - should be fixed in the next release
// of mad.
extern "C" {
struct id3_frame* id3_frame_new(char const*);
id3_length_t id3_latin1_length(id3_latin1_t const*);
void id3_latin1_decode(id3_latin1_t const*, id3_ucs4_t*);
}
#endif

struct
{
    int format;
    const wxChar* name;
    const TranslatableString desc;
}
const kFormats[] =
{
#if defined(__WXMAC__)
    {
        SF_FORMAT_AIFF | SF_FORMAT_PCM_16, wxT("AIFF"),   XO("AIFF (Apple/SGI)")
    },
#endif
    {
        SF_FORMAT_WAV | SF_FORMAT_PCM_16,  wxT("WAV"),    XO("WAV (Microsoft)")
    },
};

enum
{
#if defined(__WXMAC__)
    FMT_AIFF,
#endif
    FMT_WAV,
    FMT_OTHER
};

int LoadOtherFormat(const audacity::BasicSettings& config, int def)
{
    return config.Read(wxString("/FileFormats/ExportFormat_SF1"), def);
}

void SaveOtherFormat(audacity::BasicSettings& config, int val)
{
    config.Write(wxT("/FileFormats/ExportFormat_SF1"), val);
}

int LoadEncoding(const audacity::BasicSettings& config, int type, int def)
{
    return config.Read(wxString::Format(wxT("/FileFormats/ExportFormat_SF1_Type/%s_%x"),
                                        sf_header_shortname(type), type), def);
}

void SaveEncoding(audacity::BasicSettings& config, int type, int val)
{
    config.Write(wxString::Format(wxT("/FileFormats/ExportFormat_SF1_Type/%s_%x"),
                                  sf_header_shortname(type), type), val);
}

void GetEncodings(int type, std::vector<ExportValue>& values, TranslatableStrings& names)
{
    // Setup for queries
    SF_INFO info = {};
    info.samplerate = 44100;
    info.channels = 1;
    info.sections = 1;

    for (int i = 0, num = sf_num_encodings(); i < num; ++i) {
        int sub = sf_encoding_index_to_subtype(i);

        // Since we're traversing the subtypes linearly, we have to
        // make sure it can be paired with our current type.
        info.format = type | sub;
        if (sf_format_check(&info)) {
            // Store subtype and name
            values.emplace_back(sub);
            names.push_back(Verbatim(sf_encoding_index_name(i)));
        }
    }
}

enum : int
{
    OptionIDSFType = 0
};

ExportOptionsSFTypedEditor::ExportOptionsSFTypedEditor(int type)
    : mType(type)
{
    GetEncodings(type, mEncodingOption.values, mEncodingOption.names);

    mEncodingOption.id = type;
    mEncodingOption.title = XO("Encoding");
    mEncodingOption.flags = ExportOption::TypeEnum;
    mEncodingOption.defaultValue = mEncodingOption.values[0];

    mEncoding = *std::get_if<int>(&mEncodingOption.defaultValue);
}

std::string ExportOptionsSFTypedEditor::GetName() const
{
    return "pcm_sf_typed";
}

int ExportOptionsSFTypedEditor::GetOptionsCount() const
{
    return 1;
}

bool ExportOptionsSFTypedEditor::GetOption(int, ExportOption& option) const
{
    option = mEncodingOption;
    return true;
}

bool ExportOptionsSFTypedEditor::GetValue(ExportOptionID, ExportValue& value) const
{
    value = mEncoding;
    return true;
}

bool ExportOptionsSFTypedEditor::SetValue(ExportOptionID, const ExportValue& value)
{
    if (std::find(mEncodingOption.values.begin(),
                  mEncodingOption.values.end(), value) != mEncodingOption.values.end()) {
        mEncoding = *std::get_if<int>(&value);
        return true;
    }
    return false;
}

ExportOptionsSFTypedEditor::SampleRateList ExportOptionsSFTypedEditor::GetSampleRateList() const
{
    return {};
}

void ExportOptionsSFTypedEditor::Load(const audacity::BasicSettings& config)
{
    mEncoding = LoadEncoding(config, mType, mEncoding);
}

void ExportOptionsSFTypedEditor::Store(audacity::BasicSettings& config) const
{
    SaveEncoding(config, mType, mEncoding);
}

bool ExportOptionsSFEditor::IsValidType(const ExportValue& typeValue) const
{
    if (std::holds_alternative<int>(typeValue)) {
        const auto& typeOption = mOptions.front();
        return std::find(typeOption.values.begin(),
                         typeOption.values.end(),
                         typeValue) != typeOption.values.end();
    }
    return false;
}

ExportOptionsSFEditor::ExportOptionsSFEditor(Listener* listener)
    : mListener(listener)
{
    ExportOption typeOption {
        OptionIDSFType, XO("Header"),
        0,
        ExportOption::TypeEnum
    };

    auto hasDefaultType = false;
    for (int i = 0, num = sf_num_headers(); i < num; ++i) {
        const auto type = static_cast<int>(sf_header_index_to_type(i));
        switch (type) {
        // On the Mac, do not include in header list
#if defined(__WXMAC__)
        case SF_FORMAT_AIFF: break;
#endif
        // Do not include in header list
        case SF_FORMAT_WAV: break;
        default:
        {
            typeOption.values.emplace_back(type);
            typeOption.names.push_back(Verbatim(sf_header_index_name(i)));
            ExportOption encodingOption {
                type,
                XO("Encoding"),
                0,
                ExportOption::TypeEnum
            };
            GetEncodings(type, encodingOption.values, encodingOption.names);
            encodingOption.defaultValue = encodingOption.values[0];
            if (!hasDefaultType) {
                mType = type;
                typeOption.defaultValue = type;
                hasDefaultType = true;
            } else {
                encodingOption.flags |= ExportOption::Hidden;
            }
            mOptions.push_back(std::move(encodingOption));
            mEncodings[type] = *std::get_if<int>(&encodingOption.defaultValue);
        } break;
        }
    }
    typeOption.defaultValue = typeOption.values[0];
    mOptions.insert(mOptions.begin(), std::move(typeOption));
}

std::string ExportOptionsSFEditor::GetName() const
{
    return "pcm_sf";
}

int ExportOptionsSFEditor::GetOptionsCount() const
{
    return static_cast<int>(mOptions.size());
}

bool ExportOptionsSFEditor::GetOption(int index, ExportOption& option) const
{
    if (index >= 0 && index < static_cast<int>(mOptions.size())) {
        option = mOptions[index];
        return true;
    }
    return false;
}

bool ExportOptionsSFEditor::GetValue(ExportOptionID id, ExportValue& value) const
{
    if (id == OptionIDSFType) {
        value = mType;
        return true;
    }
    auto it = mEncodings.find(id);
    if (it != mEncodings.end()) {
        value = it->second;
        return true;
    }
    return false;
}

bool ExportOptionsSFEditor::SetValue(ExportOptionID id, const ExportValue& value)
{
    if (id == OptionIDSFType && IsValidType(value)) {
        const auto newType = *std::get_if<int>(&value);
        if (newType == mType) {
            return true;
        }

        if (mListener) {
            mListener->OnExportOptionChangeBegin();
        }

        for (auto& option : mOptions) {
            if (option.id == mType) {
                option.flags |= ExportOption::Hidden;
                if (mListener) {
                    mListener->OnExportOptionChange(option);
                }
            } else if (option.id == newType) {
                option.flags &= ~ExportOption::Hidden;
                if (mListener) {
                    mListener->OnExportOptionChange(option);
                }
            }
        }
        mType = newType;
        Store(*gPrefs);
        if (mListener) {
            mListener->OnExportOptionChangeEnd();
            mListener->OnFormatInfoChange();
        }
        return true;
    }

    auto it = mEncodings.find(id);
    if (it != mEncodings.end() && std::holds_alternative<int>(value)) {
        it->second = *std::get_if<int>(&value);
        Store(*gPrefs);
        return true;
    }
    return false;
}

ExportOptionsSFEditor::SampleRateList ExportOptionsSFEditor::GetSampleRateList() const
{
    return {};
}

void ExportOptionsSFEditor::Load(const audacity::BasicSettings& config)
{
    mType = LoadOtherFormat(config, mType);
    for (auto& p : mEncodings) {
        p.second = LoadEncoding(config, p.first, p.second);
    }

    // Prior to v2.4.0, sf_format will include the subtype.
    if (mType & SF_FORMAT_SUBMASK) {
        const auto type = mType & SF_FORMAT_TYPEMASK;
        const auto enc = mType & SF_FORMAT_SUBMASK;
        mEncodings[type] = enc;
        mType = type;
    }

    for (auto& option : mOptions) {
        const auto it = mEncodings.find(option.id);
        if (it == mEncodings.end()) {
            continue;
        }

        if (mType == it->first) {
            option.flags &= ~ExportOption::Hidden;
        } else {
            option.flags |= ExportOption::Hidden;
        }
    }
}

void ExportOptionsSFEditor::Store(audacity::BasicSettings& config) const
{
    SaveOtherFormat(config, mType);
    for (auto& [type, encoding] : mEncodings) {
        SaveEncoding(config, type, encoding);
    }
}

PCMExportProcessor::PCMExportProcessor(int subformat)
{
    context.sf = nullptr;
    context.subformat = subformat;
}

PCMExportProcessor::~PCMExportProcessor()
{
    if (context.f.IsOpened()) {
        if (context.sf != nullptr) {
            sf_close(context.sf);
        }
        context.f.Close();
    }
}

ExportPCM::ExportPCM() = default;

int ExportPCM::GetFormatCount() const
{
    return WXSIZEOF(kFormats) + 1;// + FMT_OTHER
}

FormatInfo ExportPCM::GetFormatInfo(int index) const
{
    if (index == FMT_OTHER) {
        SF_INFO si = {};
        //VS: returned format info depends on the format that was used last time.
        //That could be a source of unexpected behavior
        si.format = LoadOtherFormat(*gPrefs, kFormats[0].format & SF_FORMAT_TYPEMASK);
        si.format |= LoadEncoding(*gPrefs, si.format, kFormats[0].format);

        for (si.channels = 1; sf_format_check(&si); si.channels++) {
            // just counting
        }
        --si.channels;

        return {
            sf_header_shortname(si.format),
            XO("Other uncompressed files"),
            { sf_header_extension(si.format) },
            static_cast<unsigned>(si.channels),
            true
        };
    }

    if (!(index >= 0 && index < FMT_OTHER)) {
        index = 0;
    }

    return {
        kFormats[index].name,
        kFormats[index].desc,
        { sf_header_extension(kFormats[index].format) },
        255,
        true
    };
}

std::vector<std::string> ExportPCM::GetMimeTypes(int formatIndex) const
{
    if (formatIndex == FMT_WAV) {
        return { "audio/x-wav" };
    }
    return {};
}

// bool ExportPCM::ParseConfig(int formatIndex, const rapidjson::Value&, ExportProcessor::Parameters& parameters) const
// {
//     if (formatIndex == FMT_WAV) {
//         //no parameters available...
//         parameters = {};
//         return true;
//     }
//     return false;
// }

std::unique_ptr<ExportOptionsEditor>
ExportPCM::CreateOptionsEditor(int format, ExportOptionsEditor::Listener* listener) const
{
    if (format < FMT_OTHER) {
        return std::make_unique<ExportOptionsSFTypedEditor>(
            kFormats[format].format & SF_FORMAT_TYPEMASK);
    }
    return std::make_unique<ExportOptionsSFEditor>(listener);
}

std::unique_ptr<ExportProcessor> ExportPCM::CreateProcessor(int format) const
{
    return std::make_unique<PCMExportProcessor>(format);
}

bool PCMExportProcessor::Initialize(AudacityProject& project,
                                    const Parameters& parameters,
                                    const wxFileNameWrapper& fName,
                                    double t0, double t1, bool selectionOnly,
                                    double sampleRate, unsigned numChannels,
                                    MixerOptions::Downmix* mixerSpec,
                                    const Tags* metadata)
{
    context.t0 = t0;
    context.t1 = t1;
    context.fName = fName;

    // Set a default in case the settings aren't found
    int& sf_format = context.sf_format;

    switch (context.subformat) {
#if defined(__WXMAC__)
    case FMT_AIFF:
        sf_format = SF_FORMAT_AIFF;
        break;
#endif

    case FMT_WAV:
        sf_format = SF_FORMAT_WAV;
        break;

    default:
        // Retrieve the current format.
        sf_format = ExportPluginHelpers::GetParameterValue(parameters, OptionIDSFType, 0);
        break;
    }

    sf_format |= ExportPluginHelpers::GetParameterValue(parameters, sf_format, 0);

    // If subtype is still not specified, supply a default.
    if (!(sf_format & SF_FORMAT_SUBMASK)) {
        sf_format |= SF_FORMAT_PCM_16;
    }

    int& fileFormat = context.fileFormat;
    fileFormat = sf_format & SF_FORMAT_TYPEMASK;

    {
        wxFile& f = context.f;
        SNDFILE*& sf = context.sf;

        wxString formatStr;
        SF_INFO& info = context.info;
        //int          err;

        //This whole operation should not occur while a file is being loaded on OD,
        //(we are worried about reading from a file being written to,) so we block.
        //Furthermore, we need to do this because libsndfile is not threadsafe.
        formatStr = SFCall<wxString>(sf_header_name, fileFormat);

        // Use libsndfile to export file

        info.samplerate = (unsigned int)(sampleRate + 0.5);
        info.frames = (unsigned int)((t1 - t0) * sampleRate + 0.5);
        info.channels = numChannels;
        info.format = sf_format;
        info.sections = 1;
        info.seekable = 0;

        // Bug 46.  Trap here, as sndfile.c does not trap it properly.
        if ((numChannels != 1) && ((sf_format & SF_FORMAT_SUBMASK) == SF_FORMAT_GSM610)) {
            throw ExportException(_("GSM 6.10 requires mono"));
        }

        if (sf_format == SF_FORMAT_WAVEX + SF_FORMAT_GSM610) {
            throw ExportException(_("WAVEX and GSM 6.10 formats are not compatible"));
        }

        // If we can't export exactly the format they requested,
        // try the default format for that header type...
        //
        // LLL: I don't think this is valid since libsndfile checks
        // for all allowed subtypes explicitly and doesn't provide
        // for an unspecified subtype.
        if (!sf_format_check(&info)) {
            info.format = (info.format & SF_FORMAT_TYPEMASK);
        }
        if (!sf_format_check(&info)) {
            throw ExportException(_("Cannot export audio in this format."));
        }
        const auto path = fName.GetFullPath();
        if (f.Open(path, wxFile::write)) {
            // Even though there is an sf_open() that takes a filename, use the one that
            // takes a file descriptor since wxWidgets can open a file with a Unicode name and
            // libsndfile can't (under Windows).
            sf = sf_open_fd(f.fd(), SFM_WRITE, &info, FALSE);
            //add clipping for integer formats.  We allow floats to clip.
            sf_command(sf, SFC_SET_CLIPPING, NULL, sf_subtype_is_integer(sf_format) ? SF_TRUE : SF_FALSE);
        }

        if (!sf) {
            throw ExportException(_("Cannot export audio to %s").Format(path));
        }
        // Retrieve tags if not given a set
        if (metadata == NULL) {
            metadata = &Tags::Get(project);
        }

        // Install the meta data at the beginning of the file (except for
        // WAV and WAVEX formats)
        if (fileFormat != SF_FORMAT_WAV
            && fileFormat != SF_FORMAT_WAVEX) {
            AddStrings(sf, metadata, sf_format);
        }
        context.metadata = std::make_unique<Tags>(*metadata);

        if (sf_subtype_more_than_16_bits(info.format)) {
            context.format = floatSample;
        } else {
            context.format = int16Sample;
        }

        // Bug 2200
        // Only trap size limit for file types we know have an upper size limit.
        // The error message mentions aiff and wav.
        if ((fileFormat == SF_FORMAT_WAV)
            || (fileFormat == SF_FORMAT_WAVEX)
            || (fileFormat == SF_FORMAT_AIFF)) {
            float sampleCount = (float)(t1 - t0) * sampleRate * info.channels;
            float byteCount = sampleCount * sf_subtype_bytes_per_sample(info.format);
            // Test for 4 Gibibytes, rather than 4 Gigabytes
            if (byteCount > 4.295e9) {
                //Temporary translation hack, to say 'WAV or AIFF' rather than 'WAV'
                const auto message
                    =XO("You have attempted to Export a WAV or AIFF file which would be greater than 4GB.\n"
                        "Audacity cannot do this, the Export was abandoned.");
                throw ExportErrorException(message,
                                           wxT("Size_limits_for_WAV_and_AIFF_files"));
            }
        }

        context.status = (selectionOnly
                          ? XO("Exporting the selected audio as %s")
                          : XO("Exporting the audio as %s")).Format(formatStr);

        wxASSERT(info.channels >= 0);
        context.mixer = ExportPluginHelpers::CreateMixer(
            project, selectionOnly, t0, t1, info.channels, maxBlockLen, true,
            sampleRate, context.format, mixerSpec);
    }

    return true;
}

ExportResult PCMExportProcessor::Process(ExportProcessorDelegate& delegate)
{
    delegate.SetStatusString(context.status);

    auto exportResult = ExportResult::Success;

    {
        std::vector<char> dither;
        if ((context.info.format & SF_FORMAT_SUBMASK) == SF_FORMAT_PCM_24) {
            dither.reserve(maxBlockLen * context.info.channels * SAMPLE_SIZE(int24Sample));
        }

        while (exportResult == ExportResult::Success) {
            sf_count_t samplesWritten;
            size_t numSamples = context.mixer->Process();
            if (numSamples == 0) {
                break;
            }

            auto mixed = context.mixer->GetBuffer();

            // Bug 1572: Not ideal, but it does add the desired dither
            if ((context.info.format & SF_FORMAT_SUBMASK) == SF_FORMAT_PCM_24) {
                for (int c = 0; c < context.info.channels; ++c) {
                    CopySamples(
                        mixed + (c * SAMPLE_SIZE(context.format)), context.format,
                        dither.data() + (c * SAMPLE_SIZE(int24Sample)), int24Sample,
                        numSamples, gHighQualityDither, context.info.channels, context.info.channels
                        );
                    // Copy back without dither
                    CopySamples(
                        dither.data() + (c * SAMPLE_SIZE(int24Sample)), int24Sample,
                        const_cast<samplePtr>(mixed) // PRL fix this!
                        + (c * SAMPLE_SIZE(context.format)), context.format,
                        numSamples, DitherType::none, context.info.channels, context.info.channels);
                }
            }

            if (context.format == int16Sample) {
                samplesWritten = SFCall<sf_count_t>(sf_writef_short, context.sf, (const short*)mixed, numSamples);
            } else {
                samplesWritten = SFCall<sf_count_t>(sf_writef_float, context.sf, (const float*)mixed, numSamples);
            }

            if (static_cast<size_t>(samplesWritten) != numSamples) {
                char buffer2[1000];
                sf_error_str(context.sf, buffer2, 1000);
                //Used to give this error message
#if 0
                AudacityMessageBox(
                    XO(
                        /* i18n-hint: %s will be the error message from libsndfile, which
                         * is usually something unhelpful (and untranslated) like "system
                         * error" */
                        "Error while writing %s file (disk full?).\nLibsndfile says \"%s\"")
                    .Format(formatStr, wxString::FromAscii(buffer2)));
#else
                // But better to give the same error message as for
                // other cases of disk exhaustion.
                // The thrown exception doesn't escape but GuardedCall
                // will enqueue a message.
                GuardedCall([&]{
                    throw FileException {
                        FileException::Cause::Write, context.fName };
                });
#endif
                exportResult = ExportResult::Error;
                break;
            }
            if (exportResult == ExportResult::Success) {
                exportResult = ExportPluginHelpers::UpdateProgress(
                    delegate, *context.mixer, context.t0, context.t1);
            }
        }
    }

    // Install the WAV metata in a "LIST" chunk at the end of the file
    if (exportResult != ExportResult::Cancelled && exportResult != ExportResult::Error) {
        if (context.fileFormat == SF_FORMAT_WAV
            || context.fileFormat == SF_FORMAT_WAVEX) {
            AddStrings(context.sf, context.metadata.get(), context.sf_format);
        }
    }

    if (0 != sf_close(context.sf)) {
        // TODO: more precise message
        throw ExportErrorException("PCM:681");
    }

    context.sf = nullptr;
    context.f.Close();

    if (exportResult != ExportResult::Cancelled && exportResult != ExportResult::Error) {
        if ((context.fileFormat == SF_FORMAT_AIFF)
            || (context.fileFormat == SF_FORMAT_WAV)) {
            // Note: file has closed, and gets reopened and closed again here:
            if (!AddID3Chunk(context.fName, context.metadata.get(), context.sf_format)) {
                // TODO: more precise message
                throw ExportErrorException("PCM:694");
            }
        }
    }
    return exportResult;
}

ArrayOf<char> PCMExportProcessor::AdjustString(const wxString& wxStr, int sf_format)
{
    bool b_aiff = false;
    if ((sf_format & SF_FORMAT_TYPEMASK) == SF_FORMAT_AIFF) {
        b_aiff = true;     // Apple AIFF file
    }
    // We must convert the string to 7 bit ASCII
    size_t sz = wxStr.length();
    if (sz == 0) {
        return {};
    }
    // Size for secure allocation in case of local wide char usage
    size_t sr = (sz + 4) * 2;

    ArrayOf<char> pDest{ sr, true };
    if (!pDest) {
        return {};
    }
    ArrayOf<char> pSrc{ sr, true };
    if (!pSrc) {
        return {};
    }

    if (wxStr.mb_str(wxConvISO8859_1)) {
        strncpy(pSrc.get(), wxStr.mb_str(wxConvISO8859_1), sz);
    } else if (wxStr.mb_str()) {
        strncpy(pSrc.get(), wxStr.mb_str(), sz);
    } else {
        return {};
    }

    char* pD = pDest.get();
    char* pS = pSrc.get();
    unsigned char c;

    // ISO Latin to 7 bit ascii conversion table (best approximation)
    static char aASCII7Table[256] = {
        0x00, 0x5f, 0x5f, 0x5f, 0x5f, 0x5f, 0x5f, 0x5f,
        0x5f, 0x09, 0x0a, 0x5f, 0x0d, 0x5f, 0x5f, 0x5f,
        0x5f, 0x5f, 0x5f, 0x5f, 0x5f, 0x5f, 0x5f, 0x5f,
        0x5f, 0x5f, 0x5f, 0x5f, 0x5f, 0x5f, 0x5f, 0x5f,
        0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27,
        0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f,
        0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37,
        0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f,
        0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47,
        0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f,
        0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57,
        0x58, 0x59, 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f,
        0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67,
        0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f,
        0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77,
        0x78, 0x79, 0x7a, 0x7b, 0x7c, 0x7d, 0x7e, 0x7f,
        0x45, 0x20, 0x2c, 0x53, 0x22, 0x2e, 0x2b, 0x2b,
        0x5e, 0x25, 0x53, 0x28, 0x4f, 0x20, 0x5a, 0x20,
        0x20, 0x27, 0x27, 0x22, 0x22, 0x2e, 0x2d, 0x5f,
        0x22, 0x54, 0x73, 0x29, 0x6f, 0x20, 0x7a, 0x59,
        0x20, 0x21, 0x63, 0x4c, 0x6f, 0x59, 0x7c, 0x53,
        0x22, 0x43, 0x61, 0x22, 0x5f, 0x2d, 0x43, 0x2d,
        0x6f, 0x7e, 0x32, 0x33, 0x27, 0x75, 0x50, 0x27,
        0x2c, 0x31, 0x6f, 0x22, 0x5f, 0x5f, 0x5f, 0x3f,
        0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x43,
        0x45, 0x45, 0x45, 0x45, 0x49, 0x49, 0x49, 0x49,
        0x44, 0x4e, 0x4f, 0x4f, 0x4f, 0x4f, 0x4f, 0x78,
        0x4f, 0x55, 0x55, 0x55, 0x55, 0x59, 0x70, 0x53,
        0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x63,
        0x65, 0x65, 0x65, 0x65, 0x69, 0x69, 0x69, 0x69,
        0x64, 0x6e, 0x6f, 0x6f, 0x6f, 0x6f, 0x6f, 0x2f,
        0x6f, 0x75, 0x75, 0x75, 0x75, 0x79, 0x70, 0x79
    };

    size_t i;
    for (i = 0; i < sr; i++) {
        c = (unsigned char)*pS++;
        *pD++ = aASCII7Table[c];
        if (c == 0) {
            break;
        }
    }
    *pD = '\0';

    if (b_aiff) {
        int len = (int)strlen(pDest.get());
        if ((len % 2) != 0) {
            // In case of an odd length string, add a space char
            strcat(pDest.get(), " ");
        }
    }

    return pDest;
}

void PCMExportProcessor::AddStrings(SNDFILE* sf, const Tags* tags, int sf_format)
{
    if (tags->HasTag(TAG_TITLE)) {
        auto ascii7Str = AdjustString(tags->GetTag(TAG_TITLE), sf_format);
        if (ascii7Str) {
            sf_set_string(sf, SF_STR_TITLE, ascii7Str.get());
        }
    }

    if (tags->HasTag(TAG_ALBUM)) {
        auto ascii7Str = AdjustString(tags->GetTag(TAG_ALBUM), sf_format);
        if (ascii7Str) {
            sf_set_string(sf, SF_STR_ALBUM, ascii7Str.get());
        }
    }

    if (tags->HasTag(TAG_ARTIST)) {
        auto ascii7Str = AdjustString(tags->GetTag(TAG_ARTIST), sf_format);
        if (ascii7Str) {
            sf_set_string(sf, SF_STR_ARTIST, ascii7Str.get());
        }
    }

    if (tags->HasTag(TAG_COMMENTS)) {
        auto ascii7Str = AdjustString(tags->GetTag(TAG_COMMENTS), sf_format);
        if (ascii7Str) {
            sf_set_string(sf, SF_STR_COMMENT, ascii7Str.get());
        }
    }

    if (tags->HasTag(TAG_YEAR)) {
        auto ascii7Str = AdjustString(tags->GetTag(TAG_YEAR), sf_format);
        if (ascii7Str) {
            sf_set_string(sf, SF_STR_DATE, ascii7Str.get());
        }
    }

    if (tags->HasTag(TAG_GENRE)) {
        auto ascii7Str = AdjustString(tags->GetTag(TAG_GENRE), sf_format);
        if (ascii7Str) {
            sf_set_string(sf, SF_STR_GENRE, ascii7Str.get());
        }
    }

    if (tags->HasTag(TAG_COPYRIGHT)) {
        auto ascii7Str = AdjustString(tags->GetTag(TAG_COPYRIGHT), sf_format);
        if (ascii7Str) {
            sf_set_string(sf, SF_STR_COPYRIGHT, ascii7Str.get());
        }
    }

    if (tags->HasTag(TAG_SOFTWARE)) {
        auto ascii7Str = AdjustString(tags->GetTag(TAG_SOFTWARE), sf_format);
        if (ascii7Str) {
            sf_set_string(sf, SF_STR_SOFTWARE, ascii7Str.get());
        }
    }

    if (tags->HasTag(TAG_TRACK)) {
        auto ascii7Str = AdjustString(tags->GetTag(TAG_TRACK), sf_format);
        if (ascii7Str) {
            sf_set_string(sf, SF_STR_TRACKNUMBER, ascii7Str.get());
        }
    }
}

#ifdef USE_LIBID3TAG
struct id3_tag_deleter {
    void operator ()(id3_tag* p) const
    {
        if (p) {
            id3_tag_delete(p);
        }
    }
};
using id3_tag_holder = std::unique_ptr<id3_tag, id3_tag_deleter>;
#endif

bool PCMExportProcessor::AddID3Chunk(
    const wxFileNameWrapper& fName, const Tags* tags, int sf_format)
{
#ifdef USE_LIBID3TAG
    id3_tag_holder tp { id3_tag_new() };

    for (const auto& pair : tags->GetRange()) {
        const auto& n = pair.first;
        const auto& v = pair.second;
        const char* name = "TXXX";

        if (n.CmpNoCase(TAG_TITLE) == 0) {
            name = ID3_FRAME_TITLE;
        } else if (n.CmpNoCase(TAG_ARTIST) == 0) {
            name = ID3_FRAME_ARTIST;
        } else if (n.CmpNoCase(TAG_ALBUM) == 0) {
            name = ID3_FRAME_ALBUM;
        } else if (n.CmpNoCase(TAG_YEAR) == 0) {
            name = ID3_FRAME_YEAR;
        } else if (n.CmpNoCase(TAG_GENRE) == 0) {
            name = ID3_FRAME_GENRE;
        } else if (n.CmpNoCase(TAG_COMMENTS) == 0) {
            name = ID3_FRAME_COMMENT;
        } else if (n.CmpNoCase(TAG_TRACK) == 0) {
            name = ID3_FRAME_TRACK;
        } else if (n.CmpNoCase(wxT("composer")) == 0) {
            name = "TCOM";
        }

        struct id3_frame* frame = id3_frame_new(name);

        if (!n.IsAscii() || !v.IsAscii()) {
            id3_field_settextencoding(id3_frame_field(frame, 0), ID3_FIELD_TEXTENCODING_UTF_16);
        } else {
            id3_field_settextencoding(id3_frame_field(frame, 0), ID3_FIELD_TEXTENCODING_ISO_8859_1);
        }

        MallocString<id3_ucs4_t> ucs4{
            id3_utf8_ucs4duplicate((id3_utf8_t*)(const char*)v.mb_str(wxConvUTF8)) };

        if (strcmp(name, ID3_FRAME_COMMENT) == 0) {
            // A hack to get around iTunes not recognizing the comment.  The
            // language defaults to XXX and, since it's not a valid language,
            // iTunes just ignores the tag.  So, either set it to a valid language
            // (which one???) or just clear it.  Unfortunately, there's no supported
            // way of clearing the field, so do it directly.
            id3_field* f = id3_frame_field(frame, 1);
            memset(f->immediate.value, 0, sizeof(f->immediate.value));
            id3_field_setfullstring(id3_frame_field(frame, 3), ucs4.get());
        } else if (strcmp(name, "TXXX") == 0) {
            id3_field_setstring(id3_frame_field(frame, 2), ucs4.get());

            ucs4.reset(id3_utf8_ucs4duplicate((id3_utf8_t*)(const char*)n.mb_str(wxConvUTF8)));

            id3_field_setstring(id3_frame_field(frame, 1), ucs4.get());
        } else {
            auto addr = ucs4.get();
            id3_field_setstrings(id3_frame_field(frame, 1), 1, &addr);
        }

        id3_tag_attachframe(tp.get(), frame);
    }

    tp->options &= (~ID3_TAG_OPTION_COMPRESSION); // No compression

    // If this version of libid3tag supports it, use v2.3 ID3
    // tags instead of the newer, but less well supported, v2.4
    // that libid3tag uses by default.
#ifdef ID3_TAG_HAS_TAG_OPTION_ID3V2_3
    tp->options |= ID3_TAG_OPTION_ID3V2_3;
#endif

    id3_length_t len;

    len = id3_tag_render(tp.get(), 0);
    if (len == 0) {
        return true;
    }

    if ((len % 2) != 0) {
        len++;                  // Length must be even.
    }
    ArrayOf<id3_byte_t> buffer { len, true };
    if (buffer == NULL) {
        return false;
    }

    // Zero all locations, for ending odd UTF16 content
    // correctly, i.e., two '\0's at the end.

    id3_tag_render(tp.get(), buffer.get());

    wxFFile f(fName.GetFullPath(), wxT("r+b"));
    if (f.IsOpened()) {
        wxUint32 sz;

        sz = (wxUint32)len;
        if (!f.SeekEnd(0)) {
            return false;
        }
        if ((sf_format & SF_FORMAT_TYPEMASK) == SF_FORMAT_WAV) {
            if (4 != f.Write("id3 ", 4)) {// Must be lower case for foobar2000.
                return false;
            }
        } else {
            if (4 != f.Write("ID3 ", 4)) {
                return false;
            }
            sz = wxUINT32_SWAP_ON_LE(sz);
        }
        if (4 != f.Write(&sz, 4)) {
            return false;
        }

        if (len != f.Write(buffer.get(), len)) {
            return false;
        }

        sz = (wxUint32)f.Tell() - 8;
        if ((sf_format & SF_FORMAT_TYPEMASK) == SF_FORMAT_AIFF) {
            sz = wxUINT32_SWAP_ON_LE(sz);
        }

        if (!f.Seek(4)) {
            return false;
        }
        if (4 != f.Write(&sz, 4)) {
            return false;
        }

        if (!f.Flush()) {
            return false;
        }

        if (!f.Close()) {
            return false;
        }
    } else {
        return false;
    }
#endif
    return true;
}
